package edu.utdallas.cs.securechat.main;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.InvalidKeyException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

import javax.crypto.BadPaddingException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

import edu.utdallas.cs.securechat.utils.Logger;

public class Server {
	Map<String, String> userAccounts = new HashMap<String, String>();
	Map<String, InetAddress> onlineUsers = new HashMap<String, InetAddress>();
	Vector<String> onlineUsersList = new Vector<String>();
	ServerSocket ss = null;
	ServerProtocol sp = new ServerProtocol();
	SecretKey masterPassword;

	public Server(String masterPassword) {
		try {
			this.masterPassword = sp.keyFromPassword(masterPassword.toCharArray());
			sp.setSecretKey(this.masterPassword);
		}catch (InvalidKeySpecException e1) {
			Logger.log(Logger.ERROR, "InvalidKeySpecException: Make sure you set the master key by using -m <key>");
			System.exit(0);
		}catch (Exception e) {
			System.err.println(e);
		}
	}

	public void startServer(){
		readUsers();
		try {
			// create new server socket at port 12000, and wait for client
			// connection
			ss = new ServerSocket(12000, 10);
			Logger.log(Logger.DEBUG, "Starting server...");
			sp.generateRSAKeys();
			while (true) {
				Logger.log(Logger.INFO, "Waiting for client connection...");
				new ConnectionReceiver(ss.accept());
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				ss.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	private void writeUsers() {
		try {
			byte[] decrypted = null;
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			ObjectOutputStream output = new ObjectOutputStream(out);
			output.writeObject(userAccounts);
			output.flush();
			output.close();
			out.close();
			decrypted = out.toByteArray();
			Logger.log(Logger.DEBUG, "Master password: "+masterPassword);
			Logger.log(Logger.DEBUG, "Decrypted: "+decrypted);
			byte[] encrypted = sp.AESEncrypt(decrypted, masterPassword);
			BufferedOutputStream fileOut = new BufferedOutputStream(new FileOutputStream(new File("./users")));
			fileOut.write(encrypted);
			fileOut.close();
		} catch (Exception e) {
			System.out.println("Exception while adding key to users: " + e.getMessage());
			e.printStackTrace();
		}
	}
	
	@SuppressWarnings("unchecked")
	private void readUsers(){
		try{
			byte[] users = getUsers();
			if(users == null){
				Logger.log(Logger.ERROR, "Could not get users from the file");
				System.exit(0);
			}else if(users.length > 0){
				ByteArrayInputStream in = new ByteArrayInputStream(users);
				ObjectInputStream input = new ObjectInputStream(in);
				userAccounts = (Map<String, String>) input.readObject();
			}
		}catch(Exception e){
			e.printStackTrace();
		}
	}

	private byte[] getUsers() {
		try {
			File file = new File("./users");
			if(!file.exists()){
				if(createUsers())
					return new byte[0];
				else
					return null;
			}
			byte[] encryptedUsers = new byte[(int)file.length()];
			
			FileInputStream in = new FileInputStream(file);
			in.read(encryptedUsers);
			in.close();
			
			byte[] decryptedUsers = sp.AESDecrypt(encryptedUsers, masterPassword);
			if (decryptedUsers == null || decryptedUsers.length < 1) {
				Logger.log(Logger.INFO, "User data is empty");
				return new byte[0];
			} else {
				return decryptedUsers;
			}
		}catch(InvalidKeyException e1){
			Logger.log(Logger.ERROR, "InvalidKeyException: Make sure you have the right master key");
			Logger.log(Logger.ERROR, "System Existing...");
			System.exit(0);
			return null;
		}catch(BadPaddingException e2){
			Logger.log(Logger.ERROR, "BadPaddingException: "+"Make sure you have the right master key");
			Logger.log(Logger.ERROR, "System Existing...");
			System.exit(0);
			return null;
		}catch(NoSuchPaddingException e3){
			Logger.log(Logger.ERROR, "NoSuchPaddingExcpetion: Make sure you have the right master key");
			Logger.log(Logger.ERROR, "System Existing...");
			System.exit(0);
			return null;
		}catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	private boolean createUsers() {
		File users = new File("./users");
		try {
			return users.createNewFile();
		} catch (IOException e) {
			return false;
		}
	}

	private boolean sendMessage(Message msg) {
		String operation = msg.getOperation();
		boolean messageSent = true;
		if (operation.equals("user_logged_off")) {
			for (int i = 0; i < onlineUsers.size(); i++) {
				Socket s = null;
				ObjectInputStream in = null;
				ObjectOutputStream out = null;
				try {
					Logger.log(Logger.DEBUG, "Sending user_logged_off to: "+onlineUsers.get(msg.getOnlineUsers().get(i)));
					s = new Socket(onlineUsers.get(msg.getOnlineUsers().get(i)), 12345);
					Logger.log(Logger.DEBUG, s.toString());
					out = new ObjectOutputStream(s.getOutputStream());
					out.writeObject(msg);
					Logger.log(Logger.DEBUG, "Wrote message: " + msg);
					in = new ObjectInputStream(s.getInputStream());
					Message reply = (Message) in.readObject();
					if(reply.getOperationStatus() == false)
						messageSent = false;
					out.close();
					in.close();
					s.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}else if(operation.equals("user_logged_on")){
			for(int i = 0; i < onlineUsers.size(); i++){
				Socket s = null;
				ObjectInputStream in = null;
				ObjectOutputStream out = null;
				try {
					Logger.log(Logger.DEBUG, "Sending user_logged_on to: "+onlineUsers.get(msg.getOnlineUsers().get(i)));
					s = new Socket(onlineUsers.get(msg.getOnlineUsers().get(i)), 12345);
					Logger.log(Logger.DEBUG, s.toString());
					out = new ObjectOutputStream(s.getOutputStream());
					out.writeObject(msg);
					Logger.log(Logger.DEBUG, "Wrote message: " + msg);
					in = new ObjectInputStream(s.getInputStream());
					Message reply = (Message) in.readObject();
					if(reply.getOperationStatus() == false)
						messageSent = false;
					out.close();
					in.close();
					s.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		return messageSent;
	}

	private class ConnectionReceiver extends Thread {
		Socket s = null;
		ObjectInputStream in = null;
		ObjectOutputStream out = null;
		Message msgFromClient = null;
		Message msgToClient = null;

		public ConnectionReceiver(Socket s) {
			this.s = s;
			start();
		}

		public void run() {
			Logger.log(Logger.INFO, "Recieved: " + s);
			try {
				in = new ObjectInputStream(s.getInputStream());
				out = new ObjectOutputStream(s.getOutputStream());
				msgFromClient = (Message) in.readObject();
				Logger.log(Logger.DEBUG, msgFromClient.toString());
				if (msgFromClient.getOperation().equals("register")) {
					boolean usernameTaken = false;
					String username = msgFromClient.getUsername();
					Logger.log(Logger.INFO, "Username requested from client: " + username);
					if (userAccounts.containsKey(username)) {
						usernameTaken = true;
					} else {
						usernameTaken = false;
					}
					if (usernameTaken) {
						Logger.log(Logger.INFO, "Client trying to register a taken username");
						msgToClient = new Message("reject_registration");
						msgToClient.setStatusMessage("Usersname already take, please try another.");
						msgToClient.setOperationStatus(false);
					} else {
						byte[] password = msgFromClient.getData();
						Logger.log(Logger.DEBUG, "PreHashed Password: "+Arrays.toString(password));
						String hashedPassword = sp.hashPassword(sp.RSADecrypt(password));
						Logger.log(Logger.DEBUG, "Hashed password: "+hashedPassword);
						userAccounts.put(username, hashedPassword);
						writeUsers();
						msgToClient = new Message("accept_registration");
						msgToClient.setStatusMessage("Your account has been registered.");
						msgToClient.setOperationStatus(true);
					}
					out.writeObject(msgToClient);
				} else if (msgFromClient.getOperation().equals("login")) {
					String username = msgFromClient.getUsername();
					String realPass = userAccounts.get(username);
					String givenPass = sp.hashPassword(sp.RSADecrypt(msgFromClient.getData()));
					// Passwords match, login successful
					if (realPass == null) {
						Logger.log(Logger.INFO, "Try to login with user that doesn't exists");
						msgToClient = new Message("reject_login");
						msgToClient.setStatusMessage("Login failed, incorrect username or password.");
						msgToClient.setOperationStatus(false);
					} else {
						if (realPass.equals(givenPass)) {
							Logger.log(Logger.INFO, "User: " + username + " log in successful");
							msgToClient = new Message("accept_login");
							msgToClient.setStatusMessage("Login successful");
							Logger.log(Logger.DEBUG, "Username: " + username);
							Logger.log(Logger.DEBUG, "Real pass: " + realPass);
							Logger.log(Logger.DEBUG, "Given pass: " + givenPass);
							msgToClient.setData(sp.grantTGT(sp.keyFromPassword(username.concat(realPass).toCharArray()), username));
							msgToClient.setOperationStatus(true);
							onlineUsers.put(username, s.getInetAddress());
							Logger.log(Logger.DEBUG, s.getInetAddress().toString());
							if(!onlineUsersList.contains(username))
								onlineUsersList.add(username);
							Message updateUserListMessage = new Message("user_logged_on");
							updateUserListMessage.setOnlineUsers(onlineUsersList);
							updateUserListMessage.setUsername(username);
							boolean messageSentSuccessful = sendMessage(updateUserListMessage);
							if(messageSentSuccessful){
								Logger.log(Logger.INFO, "Update user list success");
							}else{
								Logger.log(Logger.INFO, "Could not send update user list messages to other clients");
							}
						} else {
							Logger.log(Logger.INFO, "User: " + username + " log in faliled, wrong password");
							msgToClient = new Message("reject_login");
							msgToClient.setStatusMessage("Login failed, incorrect username or password.");
							msgToClient.setOperationStatus(false);
						}
					}
					out.writeObject(msgToClient);
				} else if (msgFromClient.getOperation().equals("request_public_key")) {
					msgToClient = new Message("return_public_key");
					msgToClient.setKey(sp.getPublicKey());
					msgToClient.setOperationStatus(true);
					msgToClient.setStatusMessage("Sent server public key");
					out.writeObject(msgToClient);
				} else if (msgFromClient.getOperation().equals("request_ticket")) {
					msgToClient = new Message("grant_ticket");
					try{
						String username = msgFromClient.getUsername();
						msgToClient.setAddress(onlineUsers.get(username));
						TicketRequest request = sp.handleTicketRequest(msgFromClient.getData());
						msgToClient.setData(sp.grantTicket(request, sp.keyFromPassword(username.concat(userAccounts.get(username)).toCharArray())));
						msgToClient.setOperationStatus(true);
					} catch (Exception e) {
						msgToClient.setOperationStatus(false);
						e.printStackTrace();
					}
					out.writeObject(msgToClient);
				} else if(msgFromClient.getOperation().equals("client_exit")){
					String exitedClientName = msgFromClient.getUsername();
					onlineUsers.remove(exitedClientName);
					onlineUsersList.remove(exitedClientName);
					out.writeObject(null);
					Message updateUserListMessage = new Message("user_logged_off");
					updateUserListMessage.setOnlineUsers(onlineUsersList);
					updateUserListMessage.setUsername(exitedClientName);
					sendMessage(updateUserListMessage);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}